<?php

namespace Platform\Libs;

class MongoDBCache {

    /**
     * mongoDB 简单 封装 
     * 注意：支持 mongoDB 3.2+ 
     * 具体参数及相关定义请参见： https://docs.mongodb.com/manual/reference/command/ 
     * 
     */
    const timeOut = 1000;

    private static $_instance = [];
    private static $def = "default";
    private $_conn = null;
    private $_db = null;
    private static $_config = [
        "default" => ["url" => "mongodb://10.0.0.27:27017", "dbname" => "houwan"],
    ];
    private $log;
    private $debug = true;

    /**
     * 创建mongo对象 
     * @param  string $confkey 
     * @return \m_mgdb 
     */
    static function getInstance($confkey = NULL) {
        if (defined("CACHE_PATH")) {
            $cacheconfig = (include CACHE_PATH);
            if ($cacheconfig) {
                self::$_config['default'] = $cacheconfig['DEFAULT_MONGO_CONFIG'];
            }
        }
        if (!$confkey) {
            $confkey = self::$def;
        }
        if (!isset(self::$_instance[$confkey]) && ($conf = self::$_config[$confkey])) {
            $m = new MongoDBCache($conf);
            self::$_instance[$confkey] = $m;
        }
        return self::$_instance[$confkey];
    }

    private function __construct(array $conf) {
        $this->_conn = new \MongoDB\Driver\Manager($conf["url"]);
        $this->_db = $conf["dbname"];
        if ($this->debug) {
            if (empty(LOGS_PATH)) {
                throw new \Exception("not define logs_path!!");
            }
            if (!file_exists(LOGS_PATH . date('Y-m-d', time()))) {
                mkdir(LOGS_PATH . date('Y-m-d', time()));
            }
            $logPath = LOGS_PATH . date('Y-m-d', time());
            $file = "mongodb";
            if (!empty($file)) {
                $logFile = $logPath . "/" . $file . ".log";
            } else {
                $fileName = date('Y-m-d', time());
                $logFile = "{$logPath}/{$fileName}.log";
            }
            $this->log = new \Phalcon\Logger\Adapter\File($logFile);
        }
    }

    /**
     * 
     * 使用命令插入数据 (可以支持插入多条)
     * @param  string $collname 
     * @param  array  $documents    [["name"=>"values", ...], ...] 
     * @param  array  $writeOps     ["ordered"=>boolean,"writeConcern"=>array] 
     * @return \MongoDB\Driver\Cursor 
     */
    function commandInsert($collname, array $documents, array $writeOps = []) {
        $cmd = [
            "insert" => $collname,
            "documents" => $documents,
        ];
        $cmd += $writeOps;
        if ($this->debug) {
            $this->write_log("commandInsert：" . json_encode($cmd), "info");
        }
        return $this->command($cmd);
    }

    /**
     * 使用命令删除数据 
     * @param  string $collname 
     * @param  array  $deletes      [["q"=>query,"limit"=>int], ...] 
     * @param  array  $writeOps     ["ordered"=>boolean,"writeConcern"=>array] 
     * @return \MongoDB\Driver\Cursor 
     */
    function commandDel($collname, array $deletes, array $writeOps = []) {
        foreach ($deletes as &$_) {
            if (isset($_["q"]) && !$_["q"]) {
                $_["q"] = (Object) [];
            }
            if (isset($_["limit"]) && !$_["limit"]) {
                $_["limit"] = 0;
            }
        }
        $cmd = [
            "delete" => $collname,
            "deletes" => $deletes,
        ];
        $cmd += $writeOps;
        if ($this->debug) {
            $this->write_log("commandDel：" . json_encode($cmd), "info");
        }
        return $this->command($cmd);
    }

    /**
     * 使用命令更新数据 
     * @param  string $collname 
     * @param  array  $updates      [["q"=>query,"u"=>update,"upsert"=>boolean,"multi"=>boolean], ...] 
     * @param  array  $writeOps     ["ordered"=>boolean,"writeConcern"=>array] 
     * @return \MongoDB\Driver\Cursor 
     */
    function commandUpdate($collname, array $updates, array $writeOps = []) {
        $cmd = [
            "update" => $collname,
            "updates" => $updates,
        ];
        $cmd += $writeOps;
        if ($this->debug) {
            $this->write_log("commandUpdate：" . json_encode($cmd), "info");
        }
        return $this->command($cmd);
    }

    /**
     * 使用命令查询 
     * @param  string $collname 
     * @param  array  $filter     [query]     参数详情请参见文档 
     * @param  array  $writeOps   ["key"=>vals,...] 其它参数 参数详情请参见文档 
     * @return \MongoDB\Driver\Cursor 
     */
    function commandQuery($collname, array $filter, array $writeOps = []) {
        $cmd = [
            "find" => $collname,
            "filter" => $filter
        ];
        $cmd += $writeOps;
        if ($this->debug) {
            $this->write_log("commandQuery：" . json_encode($cmd), "info");
        }
        return $this->command($cmd);
    }

    /**
     * 执行MongoDB使用命令 
     * @param  array $param      执行的命令 
     * @return \MongoDB\Driver\Cursor 
     */
    function command(array $param) {
        $cmd = new \MongoDB\Driver\Command($param);
        if ($this->debug) {
            $this->write_log("command：" . json_encode($param), "info");
        }
        return $this->_conn->executeCommand($this->_db, $cmd);
    }

    /**
     * 获取当前mongoDB Manager 
     * @return MongoDB\Driver\Manager 
     */
    function getMongoManager() {
        return $this->_conn;
    }

    /**
     * 插入
     * @param type $collname
     * @param array $document
     * @throws Exception
     */
    function insert($collname, array $document) {
        $bulk = new \MongoDB\Driver\BulkWrite();
        $document['_id'] = new \MongoDB\BSON\ObjectID();
        $bulk->insert($document);
        $id = (string) $document['_id'];
        if ($this->debug) {
            $this->write_log("MongoDBinsert document:" . json_encode($document) . ",collname:" . $collname, "info");
        }
        $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY);
        try {
            $result = $this->_conn->executeBulkWrite($this->_db . '.' . $collname, $bulk, $writeConcern);
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
            $result = $e->getWriteResult();
            // Check if the write concern could not be fulfilled  
            if ($writeConcernError = $result->getWriteConcernError()) {
                throw new \Exception("MongodbError: " . sprintf("%s (%d): %s\n", $writeConcernError->getMessage(), $writeConcernError->getCode(), var_export($writeConcernError->getInfo(), true)));
            }
            // Check if any write operations did not complete at all  
            foreach ($result->getWriteErrors() as $writeError) {
                $Error .= sprintf("Operation#%d: %s (%d)\n", $writeError->getIndex(), $writeError->getMessage(), $writeError->getCode());
            }
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        } catch (MongoDB\Driver\Exception\Exception $e) {
            $Error = sprintf("Other error: %s\n", $e->getMessage());
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        }
        return $id;
    }

    /**
     * 更新
     * @param type $collname 表名
     * @param array $query update的查询条件，类似sql update查询内where后面的。
     * @param array $set 可以理解为sql update查询内set后面的
     * @param array $multi 可选，mongodb 默认是false,只更新找到的第一条记录，如果这个参数为true,就把按条件查出来多条记录全部更新。
     * @param array $upsert 可选，这个参数的意思是，如果不存在update的记录，是否插入objNew,true为插入，默认是false，不插入
     */
    function update($collname, $query, $set, $multi = false, $upsert = false) {
        if (!empty($query['_id'])) {
            $query['_id'] = new \MongoDB\BSON\ObjectID($query["_id"]);
        }
        $bulk = new \MongoDB\Driver\BulkWrite();
        $bulk->update($query, ['$set' => $set], ['multi' => $multi, 'upsert' => $upsert]);
        if ($this->debug) {
            $this->write_log("MongoDBupdate query:" . json_encode($query) . ",set:" . json_encode($set) . ",collname:" . $collname, "info");
        }
        $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY);
        try {
            $result = $this->_conn->executeBulkWrite($this->_db . '.' . $collname, $bulk, $writeConcern);
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
            $result = $e->getWriteResult();
            // Check if the write concern could not be fulfilled  
            if ($writeConcernError = $result->getWriteConcernError()) {
                throw new \Exception("MongodbError: " . sprintf("%s (%d): %s\n", $writeConcernError->getMessage(), $writeConcernError->getCode(), var_export($writeConcernError->getInfo(), true)));
            }
            // Check if any write operations did not complete at all  
            foreach ($result->getWriteErrors() as $writeError) {
                $Error .= sprintf("Operation#%d: %s (%d)\n", $writeError->getIndex(), $writeError->getMessage(), $writeError->getCode());
            }
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        } catch (MongoDB\Driver\Exception\Exception $e) {
            $Error = sprintf("Other error: %s\n", $e->getMessage());
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        }
        return $result->getModifiedCount();
    }

    /**
     * 删除
     * @param type $collname 表名
     * @param array $query  删除的文档的条件。
     * @param array $justOne 如果设为 true 或 1，则只删除一个文档。
     */
    function delete($collname, $query, $justOne = true) {
        $bulk = new \MongoDB\Driver\BulkWrite();
        if ($justOne) {
            $justOne = 1;
        } else {
            $justOne = 0;
        }
        $bulk->delete($query, ['limit' => $justOne]);
        $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY);
        if ($this->debug) {
            $this->write_log("MongoDBdelete query:" . json_encode($query) . ",limit:" . json_encode($justOne) . ",collname:" . $collname, "info");
        }
        try {
            $result = $this->_conn->executeBulkWrite($this->_db . '.' . $collname, $bulk, $writeConcern);
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
            $result = $e->getWriteResult();
            // Check if the write concern could not be fulfilled  
            if ($writeConcernError = $result->getWriteConcernError()) {
                throw new \Exception("MongodbError: " . sprintf("%s (%d): %s\n", $writeConcernError->getMessage(), $writeConcernError->getCode(), var_export($writeConcernError->getInfo(), true)));
            }
            // Check if any write operations did not complete at all  
            foreach ($result->getWriteErrors() as $writeError) {
                $Error .= sprintf("Operation#%d: %s (%d)\n", $writeError->getIndex(), $writeError->getMessage(), $writeError->getCode());
            }
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        } catch (MongoDB\Driver\Exception\Exception $e) {
            $Error = sprintf("Other error: %s\n", $e->getMessage());
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        }
        return $result->getDeletedCount();
    }

    /**
     * 简单查询
     * @param string $collname  表名
     * @param array $filter  查询条件
     * @param array $projection  查询字段
     * @param int $limit 查询结果条数
     * @param int $skip  偏移量
     * @param array $sort 排序  ['id': -1 ] ,['createTime': 1 ]   其中 1 为升序排列，而-1是用于降序排列
     * @param array $options  MongoDB\Driver\Query::__construct $queryOptions  文档地址 http://php.net/manual/en/mongodb-driver-query.construct.php
     * @return type
     */
    private function query($collname, $filter, array $projection = array(), int $limit = 0, int $skip = 0, array $sort = array(), array $options = array()) {
        if ($projection) {
            $options['projection'] = $projection;
        }
        if ($sort) {
            $options['sort'] = $sort;
        }
        if ($skip) {
            $options['skip'] = $skip;
        }
        if ($limit) {
            $options['limit'] = $limit;
        }
        $query = new \MongoDB\Driver\Query($filter, $options);
        $cursor = $this->_conn->executeQuery($this->_db . '.' . $collname, $query);
        if ($this->debug) {
            $this->write_log("MongoDBquery filter:" . json_encode($filter) . ",options:" . json_encode($options) . ",collname:" . $collname, "info");
        }
        $result = [];
        if ($cursor) {
            $cursor = $cursor->toArray();
            foreach ($cursor as $key => $value) {
                $value = json_decode(json_encode($value), true);
                if ($value['_id'] && isset($value['_id']['$oid'])) {
                    $value['_id'] = $value['_id']['$oid'];
                }
                $result[] = $value;
            }
        }
        return $result;
    }

    /**
     * 简单查询一条数据
     * @param string $collname  表名
     * @param array $filter  查询条件
     * @param array $projection  查询字段
     * @param array $sort 排序  ['id': -1 ] ,['createTime': 1 ]   其中 1 为升序排列，而-1是用于降序排列
     * @param array $options  MongoDB\Driver\Query::__construct $queryOptions  文档地址 http://php.net/manual/en/mongodb-driver-query.construct.php
     * @return type
     */
    public function findfirst($collname, $filter, array $projection = array(), array $sort = array(), array $options = array()) {
        $result = $this->query($collname, $filter, $projection, 1, 0, $sort, $options);
        return $result[0];
    }

    /**
     * 简单查询多条数据
     * @param string $collname  表名
     * @param array $filter  查询条件
     * @param array $projection  查询字段
     * @param int $limit 查询结果条数
     * @param int $skip  偏移量
     * @param array $sort 排序  ['id': -1 ] ,['createTime': 1 ]   其中 1 为升序排列，而-1是用于降序排列
     * @param array $options  MongoDB\Driver\Query::__construct $queryOptions  文档地址 http://php.net/manual/en/mongodb-driver-query.construct.php
     * @return type
     */
    public function find(string $collname, array $filter, array $projection = array(), int $limit = 0, int $skip = 0, array $sort = array(), array $options = array()) {
        return $this->query($collname, $filter, $projection, $limit, $skip, $sort, $options);
    }

    /**
     * 删除
     * @param type $collname 表名
     * @param array $bulkWrite \MongoDB\Driver\BulkWrite对象,具体可以查看文档。
     */
    public function executeBulkWrite($collname, $bulkWrite) {
        if ($this->debug) {
            $this->write_log("MongoDBexecuteBulkWrite bulkWrite:" . json_encode($bulkWrite) . ",collname:" . $collname, "info");
        }
        $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY);
        try {
            $result = $this->_conn->executeBulkWrite($this->_db . '.' . $collname, $bulkWrite, $writeConcern);
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
            $result = $e->getWriteResult();
            // Check if the write concern could not be fulfilled  
            if ($writeConcernError = $result->getWriteConcernError()) {
                throw new \Exception("MongodbError: " . sprintf("%s (%d): %s\n", $writeConcernError->getMessage(), $writeConcernError->getCode(), var_export($writeConcernError->getInfo(), true)));
            }
            // Check if any write operations did not complete at all  
            foreach ($result->getWriteErrors() as $writeError) {
                $Error .= sprintf("Operation#%d: %s (%d)\n", $writeError->getIndex(), $writeError->getMessage(), $writeError->getCode());
            }
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        } catch (MongoDB\Driver\Exception\Exception $e) {
            $Error = sprintf("Other error: %s\n", $e->getMessage());
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        }
        return $result;
    }

    /**
     * 批量添加
     * @param type $collname
     * @param array $documentList
     * @return type
     * @throws \Exception
     */
    public function insertAll($collname, array $documentList) {
        $bulk = new \MongoDB\Driver\BulkWrite();
        foreach ($documentList as $key => $document) {
            $document['_id'] = new \MongoDB\BSON\ObjectID();
            $bulk->insert($document);
        }
        if ($this->debug) {
            $this->write_log("MongoDBinsertAll documentList:" . json_encode($documentList) . ",collname:" . $collname, "info");
        }
        $writeConcern = new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY);
        try {
            $result = $this->_conn->executeBulkWrite($this->_db . '.' . $collname, $bulk, $writeConcern);
        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
            $result = $e->getWriteResult();
            // Check if the write concern could not be fulfilled  
            if ($writeConcernError = $result->getWriteConcernError()) {
                throw new \Exception("MongodbError: " . sprintf("%s (%d): %s\n", $writeConcernError->getMessage(), $writeConcernError->getCode(), var_export($writeConcernError->getInfo(), true)));
            }
            // Check if any write operations did not complete at all  
            foreach ($result->getWriteErrors() as $writeError) {
                $Error .= sprintf("Operation#%d: %s (%d)\n", $writeError->getIndex(), $writeError->getMessage(), $writeError->getCode());
            }
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        } catch (MongoDB\Driver\Exception\Exception $e) {
            $Error = sprintf("Other error: %s\n", $e->getMessage());
            if ($this->debug) {
                $this->write_log("MongodbError: " . $Error, "error");
            }
            throw new \Exception("MongodbError: " . $Error);
        }
        return $result->getInsertedCount();
    }

    /**
     * 统计数据
     * @param type $collname
     * @param array $filter
     * @return type
     */
    public function count($collname, array $filter) {
        $commond['count'] = $collname;
        $commond['query'] = $filter;
        if ($this->debug) {
            $this->write_log("MongoDBcount query:" . json_encode($filter) . ",collname:" . $collname, "info");
        }
        $result = $this->command($commond);
        if ($result) {
            $result = $result->toArray();
            return $result[0]->n;
        }
        return 0;
    }

    /**
     * 日志记录
     * @param $log
     * @param $level 日志等级
     * @link https://docs.phalconphp.com/zh/latest/reference/logging.html
     */
    public function write_log($log, $level = '') {
        if (is_array($log)) {
            $log = json_encode($log);
        }
        empty($level) && $level = 'error';
        $level = strtolower($level);
        $this->log->$level($log);
    }

}
